<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
        body {
            background-color: #050508;
            font-family: "Roboto Mono", monospace;
            overflow: hidden;
        }
        #container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: #050508;
        }
        .ui-panel {
            backdrop-filter: blur(10px) saturate(120%);
            -webkit-backdrop-filter: blur(10px) saturate(120%);
            background: rgba(15, 15, 20, 0.6);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        .fade-in-out {
            animation: fadeInOut 5s ease-in-out;
        }
        @keyframes fadeInOut {
            0%,
            100% {
                opacity: 0;
                transform: translate(-50%, -10px);
            }
            10%,
            90% {
                opacity: 1;
                transform: translate(-50%, 0);
            }
        }
    </style>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400&display=swap" rel="stylesheet" />
    <script type="importmap">
        {
          "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/"
          }
        }
    </script>
</head>
<body>
<div id="container"></div>
<div
        id="patternNameContainer"
        class="fixed top-6 left-1/2 -translate-x-1/2 w-full text-center pointer-events-none z-50 flex justify-center"
></div>
<div id="ui-container" class="fixed bottom-6 right-6 flex flex-col items-end gap-4 z-50">
    <button
            id="shapeButton"
            class="ui-panel group flex items-center gap-3 px-5 py-3 text-white/90 font-light rounded-lg shadow-lg hover:bg-white/10 transition-all duration-300"
    >
        <svg
                xmlns="http://www.w3.org/2000/svg"
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
                class="transition-transform duration-700 group-hover:rotate-180"
        >
            <polyline points="16 18 22 12 16 6"></polyline>
            <polyline points="8 6 2 12 8 18"></polyline>
        </svg>
        Construct
    </button>
    <div id="controlsPanel" class="ui-panel text-white/80 rounded-lg p-3 flex flex-col gap-2 shadow-lg">
        <div class="control-option flex items-center justify-between gap-3">
            <label for="autoRotateToggle" class="text-sm cursor-pointer">Auto-Rotate</label>
            <label class="relative inline-flex items-center cursor-pointer">
                <input type="checkbox" id="autoRotateToggle" class="sr-only peer" checked />
                <div
                        class="w-9 h-5 bg-gray-700/50 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-500"
                ></div>
            </label>
        </div>
        <div class="control-option flex items-center justify-between gap-3">
            <label for="animateToggle" class="text-sm cursor-pointer">Animate</label>
            <label class="relative inline-flex items-center cursor-pointer">
                <input type="checkbox" id="animateToggle" class="sr-only peer" checked />
                <div
                        class="w-9 h-5 bg-gray-700/50 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-500"
                ></div>
            </label>
        </div>
    </div>
</div>
<script type="module">
    import * as THREE from "three";
    import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
    import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
    import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
    import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";
    let scene, camera, renderer, particles, particleGroup, gridHelper;
    let composer, controls, bloomPass;
    let clock = new THREE.Clock();
    let currentPattern = 0;
    let isTransitioning = false;
    let transitionProgress = 0;
    let mouse = new THREE.Vector2(0, 0);
    let autoRotate = true;
    let animateParticlesEnabled = true;
    let particleCount = 40000;
    const transitionSpeed = 0.025;
    const patternNames = ["Voxel Cube", "Crystalline Spire", "Spherical Lattice", "Twisted Torus"];
    const particleVertexShader = `
        attribute float size;
        varying vec3 vColor;
        void main() {
            vColor = color;
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            gl_PointSize = size * (300.0 / -mvPosition.z);
            gl_Position = projectionMatrix * mvPosition;
        }
    `;
    const particleFragmentShader = `
        varying vec3 vColor;
        void main() {
            float d = length(gl_PointCoord - vec2(0.5, 0.5));
            float a = smoothstep(0.5, 0.4, d);
            if (a < 0.01) discard;
            gl_FragColor = vec4(vColor, a);
        }
    `;
    window.onload = init;
    function createVoxelCube(i, count) {
        const size = Math.cbrt(count);
        const x = (i % size) - size / 2;
        const y = (Math.floor(i / size) % size) - size / 2;
        const z = Math.floor(i / (size * size)) - size / 2;
        return new THREE.Vector3(x, y, z).multiplyScalar(3);
    }
    function createCrystallineSpire(i, count) {
        const t = i / count;
        const y = (t - 0.5) * 150;
        let r = (1 - Math.pow(t, 0.5)) * 40;
        r = Math.max(r, 0.1);
        const angle = t * 20 + Math.sin(t * Math.PI * 5) * 0.5;
        return new THREE.Vector3(r * Math.cos(angle), y, r * Math.sin(angle));
    }
    function createSphericalLattice(i, count) {
        const numPoints = 2000;
        if (i >= numPoints) return new THREE.Vector3(10000, 10000, 10000);
        const k = i + 0.5;
        const phi = Math.acos(1 - (2 * k) / numPoints);
        const theta = Math.PI * (1 + Math.sqrt(5)) * k;
        const r = 70;
        return new THREE.Vector3(r * Math.cos(theta) * Math.sin(phi), r * Math.sin(theta) * Math.sin(phi), r * Math.cos(phi));
    }
    function createTwistedTorus(i, count) {
        const numU = Math.round(Math.sqrt(count * 2));
        const numV = Math.round(count / numU);
        const u = ((i % numU) / numU) * Math.PI * 2;
        const v = (Math.floor(i / numU) / numV) * Math.PI * 2;
        const R = 60;
        const r = 20;
        const twist = 3;
        const x = (R + r * Math.cos(v)) * Math.cos(u);
        const y = (R + r * Math.cos(v)) * Math.sin(u);
        const z = r * Math.sin(v) + Math.sin(twist * u) * 15;
        return new THREE.Vector3(x, y, z);
    }
    const patterns = [createVoxelCube, createCrystallineSpire, createSphericalLattice, createTwistedTorus];
    const colorPalettes = [
        [new THREE.Color(0x00ffff), new THREE.Color(0x4dffff), new THREE.Color(0x99ffff), new THREE.Color(0xffffff)],
        [new THREE.Color(0xff00ff), new THREE.Color(0xff4dff), new THREE.Color(0xff99ff), new THREE.Color(0xffccff)],
        [new THREE.Color(0xffff00), new THREE.Color(0xffff4d), new THREE.Color(0xffff99), new THREE.Color(0xffffff)],
        [new THREE.Color(0x00ff00), new THREE.Color(0x4dff4d), new THREE.Color(0x99ff99), new THREE.Color(0xccffcc)],
    ];
    function init() {
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 4000);
        camera.position.z = 180;
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.getElementById("container").appendChild(renderer.domElement);
        setupOrbitControls();
        createBackground();
        createParticleSystem();
        setupPostProcessing();
        window.addEventListener("resize", onWindowResize);
        window.addEventListener("mousemove", onMouseMove);
        const button = document.getElementById("shapeButton");
        button.addEventListener("click", changePattern);
        button.addEventListener("touchend", (e) => {
            e.preventDefault();
            changePattern();
        });
        document.getElementById("autoRotateToggle").addEventListener("change", (e) => {
            autoRotate = e.target.checked;
            controls.autoRotate = autoRotate;
        });
        document.getElementById("animateToggle").addEventListener("change", (e) => {
            animateParticlesEnabled = e.target.checked;
        });
        updatePatternName(patternNames[currentPattern]);
        animate();
    }
    function createBackground() {
        gridHelper = new THREE.GridHelper(400, 40, 0x222233, 0x222233);
        gridHelper.position.y = -100;
        scene.add(gridHelper);
    }
    function onMouseMove(event) {
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    }
    function setupOrbitControls() {
        controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.rotateSpeed = 0.5;
        controls.zoomSpeed = 0.8;
        controls.minDistance = 30;
        controls.maxDistance = 500;
        controls.enablePan = false;
        controls.autoRotate = true;
        controls.autoRotateSpeed = 0.2;
    }
    function setupPostProcessing() {
        composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(scene, camera));
        bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.4, 0.5, 0.8);
        composer.addPass(bloomPass);
        composer.addPass(new OutputPass());
    }
    function createParticleSystem() {
        particleGroup = new THREE.Group();
        scene.add(particleGroup);
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(particleCount * 3);
        const colors = new Float32Array(particleCount * 3);
        const sizes = new Float32Array(particleCount);
        const animationParams = new Float32Array(particleCount * 2);
        const controlPoints = new Float32Array(particleCount * 3);
        const initialPattern = patterns[currentPattern];
        const initialPalette = colorPalettes[currentPattern];
        for (let i = 0; i < particleCount; i++) {
            const p = initialPattern(i, particleCount);
            positions[i * 3] = p.x;
            positions[i * 3 + 1] = p.y;
            positions[i * 3 + 2] = p.z;
            const c = initialPalette[Math.floor(Math.random() * initialPalette.length)];
            colors[i * 3] = c.r;
            colors[i * 3 + 1] = c.g;
            colors[i * 3 + 2] = c.b;
            sizes[i] = 1.0 + Math.random() * 2.0;
            animationParams[i * 2] = Math.random();
            animationParams[i * 2 + 1] = (i / particleCount) * Math.PI * 2;
        }
        geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
        geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
        geometry.setAttribute("a_anim", new THREE.BufferAttribute(animationParams, 2));
        geometry.setAttribute("a_control", new THREE.BufferAttribute(controlPoints, 3));
        const material = new THREE.ShaderMaterial({
            uniforms: { color: { value: new THREE.Color(0xffffff) } },
            vertexShader: particleVertexShader,
            fragmentShader: particleFragmentShader,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            transparent: true,
            vertexColors: true,
        });
        particles = new THREE.Points(geometry, material);
        particleGroup.add(particles);
    }
    function changePattern() {
        if (isTransitioning) return;
        const next = (currentPattern + 1) % patterns.length;
        transitionToPattern(next);
        updatePatternName(patternNames[next]);
    }
    function transitionToPattern(newPattern) {
        isTransitioning = true;
        const { position: posAttr, color: colorAttr, a_control: controlAttr } = particles.geometry.attributes;
        const fromPos = new Float32Array(posAttr.array);
        const toPos = new Float32Array(posAttr.count * 3);
        const toCol = new Float32Array(colorAttr.count * 3);
        const patternFunc = patterns[newPattern];
        const palette = colorPalettes[newPattern];
        for (let i = 0; i < posAttr.count; i++) {
            const i3 = i * 3;
            const p = patternFunc(i, posAttr.count);
            toPos[i3] = p.x;
            toPos[i3 + 1] = p.y;
            toPos[i3 + 2] = p.z;
            const c = palette[Math.floor(Math.random() * palette.length)];
            toCol[i3] = c.r;
            toCol[i3 + 1] = c.g;
            toCol[i3 + 2] = c.b;
            const p0 = new THREE.Vector3(fromPos[i3], fromPos[i3 + 1], fromPos[i3 + 2]);
            const p2 = new THREE.Vector3(toPos[i3], toPos[i3 + 1], toPos[i3 + 2]);
            const mid = p0.clone().lerp(p2, 0.5);
            const randomVec = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
            const offset = p0.distanceTo(p2) * (Math.random() * 0.5 + 0.5);
            mid.add(randomVec.multiplyScalar(offset));
            controlAttr.array[i3] = mid.x;
            controlAttr.array[i3 + 1] = mid.y;
            controlAttr.array[i3 + 2] = mid.z;
        }
        controlAttr.needsUpdate = true;
        particles.userData.fromPos = fromPos;
        particles.userData.toPos = toPos;
        particles.userData.fromCol = new Float32Array(colorAttr.array);
        particles.userData.toCol = toCol;
        particles.userData.targetPattern = newPattern;
        transitionProgress = 0;
    }
    function completeTransition() {
        const { position, color } = particles.geometry.attributes;
        position.array.set(particles.userData.toPos);
        color.array.set(particles.userData.toCol);
        position.needsUpdate = true;
        color.needsUpdate = true;
        currentPattern = particles.userData.targetPattern;
        isTransitioning = false;
    }
    function updatePatternName(name) {
        const container = document.getElementById("patternNameContainer"),
            el = document.createElement("div");
        el.className =
            "fade-in-out ui-panel bg-black/50 text-white/90 rounded-lg px-5 py-2 text-base font-light border border-white/20 shadow-lg inline-block tracking-wider";
        el.textContent = name;
        container.innerHTML = "";
        container.appendChild(el);
    }
    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        composer.setSize(window.innerWidth, window.innerHeight);
    }
    function animate() {
        requestAnimationFrame(animate);
        const delta = clock.getDelta();
        const time = clock.getElapsedTime();
        controls.update();
        if (particleGroup) {
            particleGroup.rotation.y = THREE.MathUtils.lerp(particleGroup.rotation.y, (mouse.x * Math.PI) / 40, 0.05);
            particleGroup.rotation.x = THREE.MathUtils.lerp(particleGroup.rotation.x, (-mouse.y * Math.PI) / 40, 0.05);
        }
        if (isTransitioning) {
            transitionProgress += transitionSpeed;
            if (transitionProgress >= 1.0) {
                completeTransition();
            } else {
                const { position, color, a_control } = particles.geometry.attributes;
                const { fromPos, toPos, fromCol, toCol } = particles.userData;
                const t = 0.5 * (1 - Math.cos(transitionProgress * Math.PI));
                const t1 = 1 - t;
                for (let i = 0; i < position.count; i++) {
                    const i3 = i * 3;
                    const p0x = fromPos[i3],
                        p0y = fromPos[i3 + 1],
                        p0z = fromPos[i3 + 2];
                    const p1x = a_control.array[i3],
                        p1y = a_control.array[i3 + 1],
                        p1z = a_control.array[i3 + 2];
                    const p2x = toPos[i3],
                        p2y = toPos[i3 + 1],
                        p2z = toPos[i3 + 2];
                    position.array[i3] = t1 * t1 * p0x + 2 * t1 * t * p1x + t * t * p2x;
                    position.array[i3 + 1] = t1 * t1 * p0y + 2 * t1 * t * p1y + t * t * p2y;
                    position.array[i3 + 2] = t1 * t1 * p0z + 2 * t1 * t * p1z + t * t * p2z;
                    color.array[i3] = fromCol[i3] * t1 + toCol[i3] * t;
                    color.array[i3 + 1] = fromCol[i3 + 1] * t1 + toCol[i3 + 1] * t;
                    color.array[i3 + 2] = fromCol[i3 + 2] * t1 + toCol[i3 + 2] * t;
                }
                position.needsUpdate = true;
                color.needsUpdate = true;
            }
        } else if (animateParticlesEnabled && particles) {
            animateParticles(time, delta);
        }
        composer.render(delta);
    }
    function animateParticles(time, delta) {
        const pos = particles.geometry.attributes.position.array;
        const animParams = particles.geometry.attributes.a_anim.array;
        switch (currentPattern) {
            case 0:
                const cubeSize = Math.cbrt(particleCount);
                for (let i = 0; i < particleCount; i++) {
                    const i2 = i * 2;
                    const t = Math.sin(time * 0.5 + animParams[i2] * Math.PI * 2) * 0.5 + 0.5;
                    const d = Math.max(
                        Math.abs(pos[i * 3] / 1.5 / cubeSize),
                        Math.abs(pos[i * 3 + 1] / 1.5 / cubeSize),
                        Math.abs(pos[i * 3 + 2] / 1.5 / cubeSize)
                    );
                    if (d > t) {
                        pos[i * 3 + 1] += Math.sin(animParams[i2] * 10 + time) * 0.1;
                    }
                }
                particles.geometry.attributes.position.needsUpdate = true;
                break;
            case 1:
                const pulse = Math.sin(time * 3) * 0.5 + 0.5;
                bloomPass.strength = 0.4 + pulse * 0.3;
                break;
            case 2:
                particleGroup.rotation.y += delta * 0.05;
                particleGroup.rotation.x -= delta * 0.03;
                break;
            case 3:
                for (let i = 0; i < particleCount; i++) {
                    const i2 = i * 2;
                    const i3 = i * 3;
                    const R = 60,
                        r = 20,
                        twist = 3;
                    animParams[i2 + 1] = (animParams[i2 + 1] + delta * 0.2) % (Math.PI * 2);
                    const u = animParams[i2 + 1];
                    const v = animParams[i2] * Math.PI * 2;
                    pos[i3] = (R + r * Math.cos(v)) * Math.cos(u);
                    pos[i3 + 1] = (R + r * Math.cos(v)) * Math.sin(u);
                    pos[i3 + 2] = r * Math.sin(v) + Math.sin(twist * u) * 15;
                }
                particles.geometry.attributes.position.needsUpdate = true;
                break;
        }
    }
</script>
</body>
</html>